﻿using System;
using System.IO;
using System.Threading;

namespace XamlTorrent.Managers.UriGetter.Downloaders
{
    /// <summary>
    /// Defines a segment of content and facilitates thread-safe sequential forward-only writing to a stream.
    /// </summary>
    sealed class FileSegment
    {
        /// <summary>
        /// Creates a new FileSegment instance.
        /// </summary>
        /// <param name="outputStream">Stream to which the data will be written, Stream.Length must be allocated.
        /// This reference is also used to synchronize write access to the stream.
        /// </param>
        /// <param name="segmentOffset">Start of the segment in bytes from the start.</param>
        /// <param name="segmentLength">Length of the segment in bytes.</param>
        public FileSegment(Stream outputStream, long segmentOffset, long segmentLength, long fileLength)
        {
            #region Parameter Validation
            if (outputStream == null)
                throw new ArgumentNullException("outputStream");

            if (!outputStream.CanWrite)
                throw new ArgumentException("Stream is not writable.", "outputStream");

            if (segmentOffset < 0)
                throw new ArgumentOutOfRangeException("segmentOffset");

            if (segmentLength < 0)
                throw new ArgumentOutOfRangeException("segmentLength");

            if (fileLength < 0 || (fileLength < segmentOffset+segmentLength))
                throw new ArgumentOutOfRangeException("fileLength");
            #endregion

            Offset = segmentOffset;
            Length = segmentLength;
            FileLength = fileLength;
            writeStream = outputStream;
        }
        
        /// <summary>
        /// Offset in bytes from the start of the file indicating the start of the partition.
        /// </summary>
        public readonly long Offset;

        /// <summary>
        /// Length of the partition in bytes.
        /// </summary>
        public readonly long Length;

        /// <summary>
        /// Length of the entire file.
        /// </summary>
        public readonly long FileLength;

        /// <summary>
        /// Returns Offset+Length.
        /// </summary>
        public long EndPosition
        {
            get { return (Offset + Length); }
        }

        /// <summary>
        /// True if this segments represents the whole file.
        /// </summary>
        public bool IsEntireFile
        {
            get { return (Offset == 0 && Length == FileLength); }
        }

        /// <summary>
        /// Writable and seekable stream where data will be written.
        /// </summary>
        private Stream writeStream;

        private long writtenBytes = 0;

        /// <summary>
        /// Number of bytes loaded and written to the stream.
        /// </summary>
        public long BytesWritten
        {
            get { return writtenBytes; }
        }

        /// <summary>
        /// Where in the segment we are.
        /// </summary>
        public long FilePosition
        {
            get { return (Offset + writtenBytes); }
        }

        /// <summary>
        /// Indicates whether the segment is full.
        /// </summary>
        public bool IsFull
        {
            get { return (writtenBytes >= Length); }
        }

        /// <summary>
        /// Writes the buffer to the data stream. Offset always assumed to be 0.
        /// </summary>
        /// <param name="buffer">Buffer containing data.</param>
        /// <param name="bufferOffset">How many bytes in the begining of the buffer to skip.</param>
        /// <param name="bufferLength">How many bytes to read from the buffer.</param>
        public void Write(byte[] buffer, int bufferOffset, int bufferLength)
        {
            #region Parameter Validation
            if (IsFull)
                throw new InvalidOperationException("Cannot write anymore data to segment.");

            if (buffer == null)
                throw new ArgumentNullException("buffer");

            if (bufferOffset < 0 || bufferOffset > buffer.Length)
                throw new ArgumentOutOfRangeException("bufferOffset");

            if (bufferLength < 0 || bufferLength > (buffer.Length - bufferOffset))
                throw new ArgumentOutOfRangeException("bufferLength");
            #endregion

            if (bufferLength == 0) return;

            // make sure we don't write where we're not supposed to
            long remainingBytes = (Length - writtenBytes);
            int count = (int)Math.Min((long)bufferLength, remainingBytes);

            // using Min(long, long) and not Min(int, int) because
            // remainingBytes may exceed Int32.MaxValue whereas 
            // casting Min(long, long) to Int32 is safe since
            // the first parameter's value fits into Int32.

            // To ensure Seek, Write and writtenBytes increment aren't 
            // interrupted by an exception.
            Thread.BeginCriticalRegion();

            lock (writeStream)
            {
                writeStream.Seek(FilePosition, SeekOrigin.Begin);
                writeStream.Write(buffer, bufferOffset, count);
                writtenBytes += count;
            }

            Thread.EndCriticalRegion();
        }
    }
}
